/**
 * Serial Communication 串口通信
 * 
 * 串口介绍
 * 1.串口是一种应用十分广泛的通讯接口，串口成本低、容易使用、通信线路简单，可实现两个设备的互相通信。
 * 2.单片机的串口可以使单片机与单片机、单片机与电脑、单片机与各式各样的模块互相通信，极大的扩展了单片机的应用范围，增强了单片机系统的硬件实力。
 * 3.51单片机内部自带 UART （Universal Asynchronous Receiver Transmitter，通用异步收发器），可实现单片机的串口通信。
 * 
 * 硬件电路:
 *      简单双向串口通信有两根通信线（发送端TXD和接收端RXD）: transmit exchange data
 *      2个设备的TXD与RXD要互相交叉连接
 *      当只需单向的数据传输时，可以直接一根通信线
 *      当电平标准不一致时，需要加电平转换芯片
 * 
 * 电平标准:
 *      电平标准是数据1和数据0的表达方式，是传输线缆中人为规定的电压与数据的对应关系，串口常用的电平标准有如下三种：
 *      TTL电平：+5V表示1，0V表示0              (transistor transistor logic 晶体管&晶体管 逻辑) (单片机)
 *      RS232电平：-3~-15V表示1，+3~+15V表示0   (TX-1C左下角 MAX232 芯片) (电脑, 稳定性好一些)
 *      RS485电平：两线压差+2~+6V表示1，-2~-6V表示0（差分信号）(传输线路多1倍, 但上面2种传输有距离限制(10m), 而这种可以自己根据距离控制电压[1000m]) (USB, 总线)
 * 
 * STC89C52这种型号只有1个UART
 * STC89C52的UART有四种工作模式：
 * 	    模式0：同步移位寄存器
 * 	    模式1：8位UART，波特率可变（常用）
 * 	    模式2：9位UART，波特率固定
 * 	    模式3：9位UART，波特率可变
 * 
 * 串口参数及时序图:
 *      波特率：串口通信的速率（发送和接收各数据位的间隔时间）, 每秒传输2进制代码的位数, 1波特率 = 1位/秒 (bps: 位/秒)
 *      检验位：用于数据验证
 *              无校验:
 *              奇校验: 传输的数据+校验位 的 1的个数 是奇数
 *              偶校验:
 *              1 校验:
 *              0 校验:
 *      停止位：用于数据帧间隔
 * 
 * 
 * 
 * https://www.bilibili.com/video/BV1Mb411e7re?p=20
 * 第8章 串行口通信 (来自"STC89c52.pdf" p234)
 * 1. 串行口控制寄存器SCON和PCON
 *  STC89C52系列单片机的串行口设有两个控制寄存器：串行控制寄存器SCON & 波特率选择特殊功能寄存器PCON。
 *  串行控制寄存器 SCON: 用于选择串行通信的工作方式和某些控制功能。(可位寻址)
 *  SFR name Address bit    B7    B6  B5  B4  B3  B2  B1 B0
 *    SCON     98H   name  SM0/FE SM1 SM2 REN TB8 RB8 TI RI
 * 
 * SM0/FE：当PCON寄存器中的SMOD0/PCON.6位为1时，该位用于帧错误检测。当检测到一个
 *         无效停止位时，通过UART接收器设置该位。它必须由软件清0。
 *         当PCON寄存器中的SMOD0/PCON.6位为0时，该位和SM1一起指定串行通信的工作方式
 * SM0 SM1 工作方式 功能说明                        波特率
 *  0   0   方式0   同步移位串行方式：移位寄存器    波特率是SYSclk/12,
 *  0   1   方式1   8位UART，波特率可变            ( 2^SMOD /32 )×(定时器1的'溢出率') (T1'溢出率': T1溢出的频率, 1/T, T:溢出一次所需时间)
 *  1   0   方式2   9位UART                       ( 2^SMOD / 64) x SYSclk 系统工作时钟频率
 *  1   1   方式3   9位UART，波特率可变            (2^SMOD /32 )x(定时器1的'溢出率')
 * SM2：
 *      允许方式2或方式3多机通信控制位。
 * REN: receive enable, 允许/禁止串行接收控制位。
 * TB8： 在方式2或方式3，它为要发送的第9位数据，按需要由软件置位或清0。
 * RB8： 在方式2或方式3，是接收到的第9位数据。
 * TI： 发送中断请求标志位。在方式0，当串行发送数据第8位结束时，由内部硬件自动置位，
 *      即TI=1，向主机请求中断，响应中断后必须用软件复位，即TI=0。
 *      在其他方式中，则在停止位开始发送时由内部硬件置位，必须用软件复位。
 * RI： 接收中断请求标志位。在方式0，当串行接收到第8位结束时由内部硬件自动置位RI=1，
 *      向主机请求中断，响应中断后必须用软件复位，即RI=0。
 *      在其他方式中，串行接收到停止位的中间时刻由内部硬件置位，
 *      即RI=1（例外情况见SM2说明），必须由软件复位，即RI=0。
 * 
 * 
 * PCON: 电源控制寄存器 (不可位寻址)
 * SFR name Address bit  B7   B6    B5 B4  B3  B2  B1 B0
 *   PCON   87H     name SMOD SMOD0 -  POF GF1 GF0 PD IDL
 * SMOD：
 *       波特率选择位。当用软件置位SMOD，即SMOD=1，则使串行通信方式1、2、3的波
 *       特率加倍；SMOD=0，则各工作方式的波特率不加倍。复位时SMOD=0。
 * SMOD0：帧错误检测有效控制位。当SMOD0=1，SCON寄存器中的SM0/FE位用于FE(帧错误
 *       检测)功能；当SMOD0=0，SCON寄存器中的SM0/FE位用于SM0功能,和SM1一起指
 *       定串行口的工作方式。复位时SMOD0=0
 */
#include <REGX52.H>
#include <STDARG.H>
#include <STDIO.H>
#include "Number_Utils.h"

/**
 * https://blog.csdn.net/weixin_38239856/article/details/70477138
 * 设置波特率, 算出TH1,TL1
 * 工作方式 方式1: 波特率 = ( 2^SMOD /32 )×(定时器1的'溢出率'),  (T1'溢出率': T1溢出的频率, f=1/T, T:溢出一次所需时间, 单位秒)
 *           ==> 波特率 = 2^SMOD / 32 / T
 *           ==> T = 2^SMOD / 32 / 波特率
 *           T = 2/32/4800 = 0.000013020833333333333333333333333333秒 = 13.020833333333333333333333333333us
 * 又∵ Timer1 模式2(8位自动重载) 计数1次时间 = 1s / 12MHz * 12T = 1us/次
 *      ==> 共需计数: T / 1us/次 = 13次,    8位溢出 = 2^8 = 256次(0~255)
 *   ∴ 初值 = 256 - 13 = 243
 *      TL1 = TH1 = 243 = 0xF3
 * 
 * 视频的反推:
 * 1/13us=0.07692307692307692307692307692308MHz
 * 0.07692307692307692307692307692308MHz/16=0.00480769230769230769230769230769MHz = 4807.69230769230769230769230769Hz
 * 误差 = 7.69230769230769230769230769/4800 = 0.160256410256410256410256410208%
 * @param Boud_Rate 波特率
 */
void UART_set_Boud_Rate(unsigned int Boud_Rate) {
    /**
     * T = 2^SMOD / 32 / 波特率;  //溢出一次所需时间, us
     * 2      : 2^1, 但不能这样写, ∵'^'是异或符号
     * 1.0F   : 将2转换成float
     * 1000000: 1s = 10^6us
     */
    float overFlowTime = 2 * 1.0F / 32 / Boud_Rate * 1000000;
    //频率=11.0592MHz       //12MHz:12000000
    unsigned long int frequency = 11059200;
    /**
     * Timer1 模式2(8位自动重载) 计数1次时间 = 1s / 频率f * 12T = 1us/次
     * 1000000.0F: 1s = 10^6us, 并转换成float
     */
    float t_pre_time = 1000000.0F / frequency * 12;
    //一共计数多少次
    unsigned char count = overFlowTime / t_pre_time;
    TL1 = TH1 = 256 - count;
    
    // TL1 = 0xF3;      //设置定时初始值
    // TH1 = 0xF3;      //设置定时重载值
    //11.0592MHz
    // TL1 = 0xF4;         //设置定时初始值
    // TH1 = 0xF4;         //设置定时重载值
}



/**
 * 单片机内部自带 UART （Universal Asynchronous Receiver Transmitter，通用异步收发器）串口初始化
 * STP-ISP --> 右侧选择"波特率计算器" --> 选择:
 *      系统频率       : 11.0592MHz     (误差=0.00%. 如果频率选12MHz, 不用勾选"波特率倍速", 误差=6.99%, 勾选加倍后, 误差=0.16%)
 *      波特率         : 4800           (由于晶振的原因, 收发频率过高的话, 误差会很大, ∴选择较低的波特率)
 *      UART选择       : 串口1(通用)    (STC89C52只有1个串口)
 *      UART数据位     : 8位数据        (不要校验位)
 *      波特率发生器    : 定时器1(8位自动重载)
 *      定时器时钟      : 12T(POSC/12)  (STC89C52只有12T, 没有1T) (12T: 每 12 个时钟加 1 )
 *      波特率倍速(SMOD): ☑            (波特率加倍)
 * 
 * PCON: 电源控制寄存器
 * SCON: 串行控制寄存器
 * TMOD: 定时器/计数器 工作模式寄存器, 设置 定时器0:
 *       GATE  C/T  M1  M0    GATE  C/T  M1  M0
 *       \   定时器1    /     \   定时器0    /
 *       M1=1, M0=0: 定时器模式: 8位自动重装载定时器，当溢出时将TH0存放的值自动重装入TL0
 * 
 * @param receiveEnable 是否要通过串口接收数据
 *        如果需要中断, 需要自己写中断函数, 见底部 {@link void UART_Routine()} 函数示例.
 * @param Boud_Rate 波特率
 * 
 * 使用串口通信: 
 *   打开 stc-isp 烧录软件 --> USB-CDC/串口助手 --> 勾选'HEX模式',选择串口,波特率,校验 --> 打开串口
 *   还有其它的串口助手, 网上搜
 *   也可以自己用上位机编写自己的串口程序.
 */
void UART_Init(bit receiveEnable, unsigned int Boud_Rate)		//4800bps@11.0592MHz
{
    //串口部分
	PCON |= 0x80;		//使能波特率倍速位SMOD          (1000 0000, SMOD=1)
    if (receiveEnable) {
	    SCON = 0x50;    //8位数据,可变波特率            (0101 0000, SM0=0, SM1=1,  REN=1, TI=RI=0(发送接收中断初始化) )
    } else {
	    SCON = 0x40;    //8位数据,可变波特率            (0100 0000, SM0=0, SM1=1,  REN=0, TI=RI=0(发送接收中断初始化) )
    }
    
	
    //定时器部分
	// AUXR &= 0xBF;    //定时器时钟12T模式         (89C52 没有 AUXR 辅助寄存器, 89C52已经是12T模式) (12T: 每 12 个时钟加 1 )
	// AUXR &= 0xFE;    //串口1选择定时器1为波特率发生器
    TMOD &= 0x0F;		//设置定时器模式                (定时器1 高4位清0)
	TMOD |= 0x20;		//设置定时器模式                (0010 0000, 定时器1 高4位0010)
    
    //设置波特率, 算出TH1,TL1
    UART_set_Boud_Rate(Boud_Rate);

	ET1 = 0;            //禁止定时器中断                (enable T1 interrupt, T1的溢出中断允许位。ET1=1，允许T1中断；ET1=0禁止T1中断)
                        //                              ∵只做溢出的波特率发生器, ∴不需要中断, 也不用写中断的配置(TF1, EA, PT1)(不用写中断函数)
	TR1 = 1;            //定时器1开始计时               (timer run)

    //接收数据才设置中断
    if (receiveEnable) {
        //enable serial, 串口中断
        ES = 1;
        //enable all, 总中断(所有中断的总开关)
        EA = 1;
        //串口中断的优先级
        // PS = 0;
    }
}


/**
 * 发送1个字节
 * TI： 发送中断请求标志位。在方式0，当串行发送数据第8位结束时，由内部硬件自动置位，
 *      即TI=1，向主机请求中断，响应中断后必须用软件复位，即TI=0。
 *      在其他方式中，则在停止位开始发送时由内部硬件置位，必须用软件复位。
 * RI： 接收中断请求标志位。在方式0，当串行接收到第8位结束时由内部硬件自动置位RI=1，
 *      向主机请求中断，响应中断后必须用软件复位，即RI=0。
 *      在其他方式中，串行接收到停止位的中间时刻由内部硬件置位，
 *      即RI=1（例外情况见SM2说明），必须由软件复位，即RI=0。
 * @param 发送的1个字节
 */
void UART_sendByte(unsigned char Byte) {
    SBUF = Byte;
    while (TI == 0);    //等待发送完成
    TI = 0;
}

/**
 * 换行
 */
void UART_sendNewLine() {
    UART_sendByte('\r');
    UART_sendByte('\n');
}

/**
 * 发送数字
 * @param num 0~4294967295 (2^32-1)
 * @param endNewLine 是否在结尾加上换行符
*/
void UART_sendNum(unsigned long int num, bit endNewLine) {
    unsigned char len;
    unsigned long int divider;
    len = Number_Len(num);
    divider = Number_Pow(10, len - 1);
    for (; len--; divider /= 10) {
        UART_sendByte(num / divider % 10 + '0');
    }
    if (endNewLine) UART_sendNewLine();
}

/**
 * 发送字符串
 * @param str 字符串, 不是数组
 * @param endNewLine 是否在结尾加上换行符
 */
void UART_sendString(char* str, bit endNewLine) {
    unsigned char i = 0;
    while (str[i]) {
        UART_sendByte(str[i++]);
       //如果是12MHz有误差, 波特率对不上, 那么延迟一下就正确了
    //    Delayms(300);
    }
    if (endNewLine) UART_sendNewLine();
}

/**
 * 发送格式化字符串, 注意: 整数默认是unsigned int, 即%bu
 * @param endNewLine 是否在结尾加上换行符
 * @param Format 格式化字符串,         示例: "a=%bu, b=%c, c=%f"
 * @param ... 要填入格式化字符串的参数, 示例: 123, 'A', 12.345F
 */
void UART_sendStringFormat(bit endNewLine, char* Format, ...) {
    //TODO 直接打印不行, 可能要配置啥东西
    // printf("%d+%d=%d\n", 2, 3, 5);
    //TODO 要使用xdata&传入数组...
    xdata char Dest[1];
    va_list args;
    va_start(args, Format);
    vsprintf(Dest, Format, args);
    va_end(args);
    // while (Dest[i]) UART_sendByte(Dest[i++]);        //可行
    // if (endNewLine) UART_sendNewLine();
    // UART_sendNum(sizeof(Dest) / sizeof(Dest[0]), 1); //=1
    // UART_sendNum(strlen(Dest), 1);                   //=正确长度
    UART_sendString(Dest, endNewLine);
}

/**
 * 发送float, double
 * @param showSigned 是否在 float/double 前显示 "+" 或 "-" 号
 * @param Double float/double
 * @param endNewLine 是否在结尾加上换行符
 */
void UART_sendDouble(bit showSigned, double Double, bit endNewLine) {
    if (showSigned) {
        UART_sendStringFormat(endNewLine, "%+.7g", Double);
    } else UART_sendStringFormat(endNewLine, "%.7g", Double);
}

/**
 * 发送16进制
 * @param Number 发送的数字
 * @param endNewLine 是否在结尾加上换行符
 */
void UART_sendHexNum(unsigned long int Number, bit endNewLine) {
    UART_sendStringFormat(endNewLine, "%.lX", Number);
}



static void (*UART_onTickListener)(unsigned char ch);
/**
 * if 要接收数据, 设置 UART 的监听
 * 工程中写多个 interrupt 4 会报错, 所以写成监听模式
 * TODO 写成 addListener 模式
 * @param UART_onTick 回调的函数指针, 示例: <br />
 *        void UART_onTick(unsigned char ch) {
 *            //ch就是接收到的数据
 *        }
 *        //传入函数指针
 *        UART_setOnTickListener(&UART_onTick);
 */
void UART_setOnTickListener(void (*UART_onTick)(unsigned char ch)) {
    UART_onTickListener = UART_onTick;
}

/**
 * 接收数据, UART中断 的中断号是4
 */
static void UART_Routine() interrupt 4 {
    if (RI == 1) {
        if (UART_onTickListener) UART_onTickListener(SBUF);
        //要关掉, 否则第1次接收数据后就接收不到了
        RI = 0;
    }
}
